package multithread.framework.threadmethod;

import static multithread.framework.constant.XMLConfigConstant.*;

import java.io.File;
import java.io.FileNotFoundException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import multithread.framework.annotation.ThreadAfter;
import multithread.framework.annotation.ThreadAllAfter;
import multithread.framework.annotation.ThreadAllBefore;
import multithread.framework.annotation.ThreadBefore;
import multithread.framework.annotation.ThreadFor;
import multithread.framework.annotation.ThreadGroupAfter;
import multithread.framework.annotation.ThreadGroupBefore;
import multithread.framework.annotation.ThreadGroupFor;
import multithread.framework.annotation.factory.AnnotationFactory;
import multithread.framework.exception.ConfigurationException;
import multithread.framework.utility.Utilities;
import multithread.framework.utility.convert.TypeConvert;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.input.SAXBuilder;

/**
 * Parse specified XML config file. 
 * <li>Extract <code>Method</code> objects from specified test case class object.
 * <li>Wrap each <code>Method</code> object into <code>ThreadMethod</code> with it's sticked annotations, parameter values. 
 * <li>Each <code>ThreadMethod</code> object will be added to a <code>ThreadMethodContainer</code> object,
 *     one <code>ThreadMethodContainer</code> per CASE.
 * <li>Each <code>ThreadMethodContainer</code> object will be added to a <code>TestCaseContainer</code>
 * <p>There are two type of configure files:
 * <li>The entry configure file, it contains a "suite" element as the root element of this xml file. It also may contains several
 *     "case" elements, here the "case" element can be a reference case, that means this element has a "ref" attribute referenced
 *     to a external configure file; or this "case" element can be a actually a "case", has actualy test case deined in it.
 * <li>The common configure file, it only contains a "case" element as the root element
 * 
 * <p>Each test case is wrappered into a <code>ThreadMethodContainer</code>,
 * this container contains the test case class object and related <code>ThreadMethod</code>s
 * and then add this <code>ThreadMethodContainer</code> into a <code>TestCaseContainer</code>.
 * There
 * @author rico_yu
 * @version 1.0
 * @since 12/21/2011
 */
public class XMLThreadMethodParser implements IParser<IContainer> {
	private Log log = LogFactory.getLog(XMLThreadMethodParser.class);
	private IParseFilter<Method> parseFilter = null;
	
	//for methods that have primitive parameter types
	@SuppressWarnings("rawtypes")
	private static final Map<String, Class> PRIMITIVE_TYPES = new HashMap<String, Class>();
	static {
		PRIMITIVE_TYPES.put("int", int.class);
		PRIMITIVE_TYPES.put("char", char.class);
		PRIMITIVE_TYPES.put("long", long.class);
		PRIMITIVE_TYPES.put("double", double.class);
		PRIMITIVE_TYPES.put("float", float.class);
		PRIMITIVE_TYPES.put("boolean", boolean.class);
		PRIMITIVE_TYPES.put("byte", byte.class);
		PRIMITIVE_TYPES.put("short", short.class);
	}
	
	public XMLThreadMethodParser(){}
	
	public XMLThreadMethodParser(IParseFilter<Method> parseFilter){
		this.parseFilter = parseFilter;
	}

	/**
	 * Construct a <code>ThreadMethodContainer</code> object from XML configuration file
	 * @param configFile The config file path
	 */
	@SuppressWarnings("unchecked")
	@Override
	public IContainer parse(Object configFile) {
		//param configFile should be a String
		String configFilePath = (String) configFile;

		File file = createFile(configFilePath);
		Document doc = buildDocument(file);
		TestCaseContainer testCaseContainer = TestCaseContainer.newInsatnce();

		// the "suite" element
		Element rootElement = doc.getRootElement();
		checkRootElement(rootElement);
		/**
		 * get all "case" elements of this config file
		 * this caseElements is type of: org.jdom.ContentList$FilterList
		 * and if try to add element of different config file to this list, an exception will be thrown:
		 * org.jdom.IllegalAddException: The element "case" could not be added as the root of the document: The Content already has an existing parent document
		 * so will add all the elements of this list to a newly created <code>ArrayList</code>
		 */
		List<Element> caseElements = rootElement.getChildren(CASE_ELEMENT);
		// in main config file, if one case has a "ref" attribute, read the
		// referenced config file, and delete this case element
		List<Element> caseElements2Delete = new ArrayList<Element>();
		// store case elements that defined in separate config files
		List<Element> externalCaseElements = new ArrayList<Element>();
		
		//find all case elements that contain "ref" attribute
		for (Iterator<Element> iterator = caseElements.iterator(); iterator.hasNext();) {
			Element caseElement = iterator.next();
			String ref = caseElement.getAttributeValue(REF_PROPERTY);
			if (null != ref) {// this case element is like this: <case ref="case2.xml"/>
				File externalFile = getExternalFile(file, ref);
				Element extCaseElement = getCaseElement(externalFile);

				String repeat = caseElement.getAttributeValue(REPEAT_PROPERTY);
				if (repeat != null) {
					try {
						repeat = repeat.trim();
						Long.valueOf(repeat);
						extCaseElement.setAttribute(REPEAT_PROPERTY, repeat);
					} catch (NumberFormatException e) {
						throwConfigurationException("Error when cast "+REPEAT_PROPERTY+" property from string value \""+repeat+"\" to long value");
					}
				}

				// add each external config file case to this temporary list
				externalCaseElements.add(extCaseElement);
				// for this caseElement is a "ref" case element, delete it later
				caseElements2Delete.add(caseElement);
			}
		}
		// delete case element with a attribute "ref"
		caseElements.removeAll(caseElements2Delete);
		List<Element> allCaseElements = new ArrayList<Element>();
		allCaseElements.addAll(caseElements);
		allCaseElements.addAll(externalCaseElements);
		
		// iterate all cases
		for (Iterator<Element> iterator = allCaseElements.iterator(); iterator.hasNext();) {
			Element caseElement = iterator.next();
			ThreadMethodContainer container = handleCaseElement(caseElement);

			long repeat = 1;
			String repeatVal = caseElement.getAttributeValue(REPEAT_PROPERTY);
			if (repeatVal != null) {
				repeat = Long.valueOf(repeatVal);
			}
			for (long i = 0; i < repeat; i++) {
				testCaseContainer.addThreadMethodContainer(container);
			}

		}

		return testCaseContainer;
	}

	

	/**
	 * Construct a <code>Class</code> object for this "case",
	 * get "common" element of this case, extract all methods defined under "common" element
	 * get all "group" elements of this case,, extract all methods defined under "group" element
	 * @param caseElement the case element
	 * @return ThreadMethodContainer contains all methods defined in this case
	 */
	@SuppressWarnings("unchecked")
	private ThreadMethodContainer handleCaseElement(Element caseElement) {
		IThreadMethodChecker checker = new ThreadMethodCheckerImpl();
		// each "case" will be added to a container
		ThreadMethodContainer container = ThreadMethodContainer.newInsatnce(checker);
		// each case element has a class element, get the class name
		String className = caseElement.getChildText(CLASS_ELEMENT);
		debug("class name is: " + className);
		// get the class object
		Class<?> targetClass = null;
		try {
			targetClass = Class.forName(className);
		} catch (ClassNotFoundException e) {
			log.error("Error when load class " + className, e);
			throw new RuntimeException(e);
		}
		container.setTestCaseClass(targetClass);

		// get timeout
		String timeout = caseElement.getChildText(TIMEOUT_ELEMENT);
		if (null != timeout) {
			try {
				container.setThreadMethodTimeout(Long.valueOf(timeout));
			} catch (NumberFormatException e) {
				throwConfigurationException("Error when cast " + timeout + " from String to Long", e);
			}
		}

		// get the "common" element
		Element commonElement = caseElement.getChild(COMMON_ELEMENT);
		if (commonElement != null)
			handleCommonElement(container, targetClass, commonElement);

		// get all "group" elements
		List<Element> groups = caseElement.getChildren(GROUP_ELEMENT);
		handleGroups(container, targetClass, groups);

		return container;
	}


	/**
	 * Construct <code>ThreadGroupFor</code> object for each "group" element.
	 * get all "methods" elements of this "group"
	 * @param container
	 * @param targetClass
	 * @param groups
	 */
	@SuppressWarnings({ "rawtypes", "unchecked" })
	private void handleGroups(ThreadMethodContainer container, Class<?> targetClass,
			List<Element> groups) {
		for (Iterator iterator2 = groups.iterator(); iterator2.hasNext();) {
			Element groupElement = (Element) iterator2.next();
			// get "type" attribute value of this "group" element
			String groupType = groupElement.getAttributeValue(TYPE_PROPERTY);
			int groupTypeValue = checkGroupType(groupType);
			// this annotation will be set to ThreadMethod
			ThreadGroupFor threadGroupFor = null;
			// ThreadGroupFor annotation is added
			if (groupTypeValue == THREAD_GROUP_FOR_TYPE) {
				// get the group name if specified
				String name = groupElement.getAttributeValue(NAME_PROPERTY);
				if (null != name) {// specified, create an ThreadGroupFor
									// instance with specified name
					threadGroupFor = AnnotationFactory.createThreadGroupFor(name);
				} else {// name attribute is not specified, create an
						// ThreadGroupFor instance with a default value
					threadGroupFor = (ThreadGroupFor) AnnotationFactory
							.getAnnotationInstance(groupTypeValue);
				}
			}

			// get all methods element
			List<Element> methodsElements = groupElement.getChildren(MEHTODS_ELEMENT);
			handleMethods(container, targetClass, threadGroupFor, methodsElements);
		}
	}

	/**
	 * Process each method element here.
	 * extract <code>Method</code> object into <code>ThreadMethod</code> from each "method" element,
	 * set annotation properties and methodParams of this <code>ThreadMethod</code> object,
	 * then add <code>ThreadMethod</code> object into <code>ThreadMethodContainer/code>.
	 * @param container
	 * @param targetClass
	 * @param threadGroupFor
	 * @param methodsElements
	 */
	@SuppressWarnings({ "rawtypes", "unchecked" })
	private void handleMethods(ThreadMethodContainer container, Class<?> targetClass,
			ThreadGroupFor threadGroupFor, List<Element> methodsElements) {
		for (Iterator it = methodsElements.iterator(); it.hasNext();) {
			Element methodsElement = (Element) it.next();

			// get all method element defined in this methods element
			List<Element> methodElements = methodsElement.getChildren(METHOD_ELEMENT);
			for (Iterator it2 = methodElements.iterator(); it2.hasNext();) {
				Element methodElement = (Element) it2.next();
				// get method name
				String methodName = methodElement.getAttributeValue(NAME_PROPERTY);
				/**
				 * "method" element that under "group" element, will only has
				 * ThreadFor annotation. we only need to check if "thread"
				 * property is specified, if does, use specified thread count wo
				 * create a ThreadFor annotation, else, use default value
				 */
				ThreadFor threadFor = initializeThreadFor(methodElement);

				// get parameter types and values
				List<Class> parameterTypes = new ArrayList<Class>();
				List<Object> parameterValues = new ArrayList<Object>();
				setMethodParameter(methodElement, parameterTypes, parameterValues);

				// get method object
				Method method = getMethod(targetClass, methodName, parameterTypes);

				if (parseFilter != null && !parseFilter.accept(method))
					continue;

				ThreadMethod threadMethod = new ThreadMethod(method);
				Object[] paramValues = new Object[parameterValues.size()];
				// set method parameters
				parameterValues.toArray(paramValues);
				setThreadMethodProperty(threadGroupFor, threadFor, threadMethod, paramValues);

				container.addThreadMethod(threadMethod);
			}
		}
	}

	/**
	 * If "thread" property is specified by "method" element , create a ThreadFor instance with specified thread count;
	 * Otherwise, create a default ThreadFor instance
	 * @param methodElement
	 * @return ThreadFor 
	 */
	private ThreadFor initializeThreadFor(Element methodElement) {
		String threadCount = methodElement.getAttributeValue(THREAD_PROPERTY);
		// user specified thread attribute
		if (null != threadCount) {
			// if specified, cann't be empty
			try {
				int count = Integer.parseInt(threadCount.trim());
				if (count < 1) {
					throwConfigurationException("Minmum thread count is 1! current value is:"
							+ count);
				}
				return AnnotationFactory.createThreadFor(count);
			} catch (NumberFormatException e) {
				log.error("Error when cast: " + threadCount + " to an int value", e);
				throw new RuntimeException(e);
			}
		} else {
			return (ThreadFor) AnnotationFactory.getAnnotationInstance(THREAD_FOR_TYPE);
		}
	}

	/**
	 * get the method object of specified target class with specified parameter types
	 * @param targetClass
	 * @param methodName
	 * @param parameterTypes
	 * @return Method method object of this targetClass with specified mathodName and parameter types
	 */
	@SuppressWarnings("rawtypes")
	private Method getMethod(Class<?> targetClass, String methodName, List<Class> parameterTypes) {
		Class<?>[] paramTypes = new Class<?>[parameterTypes.size()];
		parameterTypes.toArray(paramTypes);
		Method method = null;
		try {
			method = targetClass.getDeclaredMethod(methodName, paramTypes);
		} catch (Exception e) {
			log.error("Error when find method: " + methodName + " with parameter types: "
					+ paramTypes, e);
			throw new RuntimeException(e);
		}
		return method;
	}

	/**
	 * extract method parameter types and values into parameterTypes and parameterValues
	 * @param methodElement
	 * @param parameterTypes
	 * @param parameterValues
	 */
	@SuppressWarnings({ "unchecked", "rawtypes" })
	private void setMethodParameter(Element methodElement, List<Class> parameterTypes,
			List<Object> parameterValues) {
		Element paramsElement = methodElement.getChild(POARAMS_ELEMENT);
		if (paramsElement != null) {
			List<Element> paramElements = paramsElement.getChildren(PARAM_ELEMENT);
			for (Iterator iterator5 = paramElements.iterator(); iterator5.hasNext();) {
				Element param = (Element) iterator5.next();
				extractNonArr(parameterTypes, parameterValues, param);
				extractArr(parameterTypes, parameterValues, param);
			}
		}
	}

	/**
	 * Extract parameters which is of array type of this method
	 * @param parameterTypes
	 * @param parameterValues
	 * @param paramElement
	 */
	@SuppressWarnings({ "rawtypes", "unchecked" })
	private void extractArr(List<Class> parameterTypes, List<Object> parameterValues, Element paramElement) {
		String paramElementType = paramElement.getAttributeValue(ELEMENT_TYPE_PROPERTY);
		//if "elementType" property is not specified, indicate that this param is not array type
		//so here we need not to process
		if(null == paramElementType)
			return;
		paramElementType = paramElementType.trim();
		Class<?> clazz = PRIMITIVE_TYPES.get(paramElementType);//element type of array is primitive type
		if (clazz == null) {// parameter is not primitive type
			clazz = String.class; //currently only support primitive and string
		}
		
		/**
		 * If "value" attribute and "values" element both not specified,
		 * consider this array parameter as a empty array
		 */
		String value = paramElement.getAttributeValue(VALUE_PROPERTY);
		String[] strArr = new String[0];//temporary store array values, default to 0 length
		if(null != value){//"value" property is specified
			strArr = value.split(COMMA_SEPARATOR);
		}else{//try to read "values" element
			Element valuesElement = paramElement.getChild(VALUES_ELEMENT);
			if(valuesElement!=null){
				//"values" element may contain children: "value","null"
				List<Element> elements = (List<Element>)valuesElement.getChildren();
				List<String> tempValues = new ArrayList<String>();//temporary store all "value"/"null" element's values
				if(!elements.isEmpty()){
					for (Iterator iterator = elements.iterator(); iterator.hasNext();) {
						Element element = (Element)iterator.next();
						String elementName = element.getName();
						if(VALUE_ELEMENT.equals(elementName)){
							tempValues.add(element.getText());
						}else if(NULL_ELEMENT.equals(elementName)){
							tempValues.add(null);
						}
					}
				}
				strArr = new String[tempValues.size()];
				tempValues.toArray(strArr);
			}
		}
		
		//begin to convert elements of strArr to 
		if(clazz.isPrimitive()){
			Object targetArr = TypeConvert.convert(clazz, strArr);
			parameterTypes.add(targetArr.getClass());
			parameterValues.add(targetArr);
		}else{//of string types
			parameterTypes.add(strArr.getClass());
			parameterValues.add(strArr);
		}
	}

	/**
	 * Extract parameters which is not array type of this method
	 * @param parameterTypes
	 * @param parameterValues
	 * @param param
	 */
	@SuppressWarnings("rawtypes")
	private void extractNonArr(List<Class> parameterTypes, List<Object> parameterValues,
			Element param) {
		String paramClassName = param.getAttributeValue(CLASS_PROPERTY);
		//if "class" property is not specified, indicate that this param is of array type
		//so here we need not to process
		if(null == paramClassName)
			return;
		String paramValueStr = param.getAttributeValue(VALUE_PROPERTY);
		paramClassName = paramClassName.trim();
		Class<?> clazz = PRIMITIVE_TYPES.get(paramClassName);
		if (clazz == null) {// parameter is not primitive type
			clazz = findClass(paramClassName);
		}
		parameterTypes.add(clazz);
		if (clazz == String.class) {
			parameterValues.add(paramValueStr);
		} else if (clazz.isPrimitive()) {
			/*if (clazz == int.class) {
				parameterValues.add(Integer.valueOf(paramValueStr));
			} else if (clazz == long.class) {
				parameterValues.add(Long.valueOf(paramValueStr));
			} else if (clazz == float.class) {
				parameterValues.add(Float.valueOf(paramValueStr));
			} else if (clazz == double.class) {
				parameterValues.add(Double.valueOf(paramValueStr));
			} else if (clazz == boolean.class) {
				parameterValues.add(Boolean.valueOf(paramValueStr));
			} else if (clazz == byte.class) {
				parameterValues.add(Byte.valueOf(paramValueStr));
			} else if (clazz == short.class) {
				parameterValues.add(Short.valueOf(paramValueStr));
			} else if (clazz == char.class) {
				parameterValues.add(paramValueStr.charAt(0));
			}*/
			parameterValues.add(TypeConvert.convert(clazz, paramValueStr));
		}
	}

	private void setThreadMethodProperty(ThreadGroupFor threadGroupFor, ThreadFor threadFor,
			ThreadMethod threadMethod, Object[] paramValues) {
		threadMethod.setMethodParams(paramValues);
		threadMethod.setThreadFor(threadFor);
		threadMethod.setThreadGroupFor(threadGroupFor);
	}

	
	/**
	 * type value of group element in config file should only be 0: NoneThreadGroup, 1: ThreadGroupFor
	 * if not specify, default to 0
	 * @param groupType
	 */
	private int checkGroupType(String groupType) {
		// if "type" attribute id not specified, default to 0, that is:
		// ThreadGroupFor annotation is not added
		if (null == groupType || groupType.trim().equals("")) {
			return NONE_THREAD_GROUP_TYPE;
		}
		try {
			int groupTypeValue = Integer.parseInt(groupType.trim());
			if (groupTypeValue != NONE_THREAD_GROUP_TYPE
					&& groupTypeValue != THREAD_GROUP_FOR_TYPE) {
				throwConfigurationException("type attribute of group element should only be 0: NoneThreadGroup, 1: ThreadGroupFor! current value is: "
						+ groupTypeValue);
			}
			return groupTypeValue;
		} catch (NumberFormatException e) {
			log.error("type attribute of group element should only be 0: NoneThreadGroup, 1: ThreadGroupFor! current value is: "
					+ groupType.trim());
			throw new ConfigurationException(e);
		}
	}
	
	/**
	 * type attribute in method element is mandatory
	 * @param methodType
	 * @return An array of method types
	 */
	@SuppressWarnings("unused")
	private int[] checkMethodType(String methodType) {
		if (null == methodType || methodType.trim().equals("")) {
			throw new ConfigurationException("type attribute of method element is mandatory!");
		}
		try {
			// type attribute in method element may be like: 2,3,4,5
			String[] valuesStr = methodType.trim().split(",");
			int[] methodTypeValues = new int[valuesStr.length];
			for (int i = 0; i < valuesStr.length; i++) {
				int methodTypeValue = Integer.parseInt(valuesStr[i].trim());

				if (methodTypeValue != THREAD_BEFORE_TYPE && methodTypeValue != THREAD_AFTER_TYPE
						&& methodTypeValue != THREAD_GROUP_BEFORE_TYPE
						&& methodTypeValue != THREAD_GROUP_AFTER_TYPE
						&& methodTypeValue != THREAD_ALL_BEFORE_TYPE
						&& methodTypeValue != THREAD_ALL_AFTER_TYPE) {
					String errorMessage = "type attribute of method element should only be 2:ThreadBefore 3:ThreadAfter 4:ThreadGroupBefore 5:ThreadGroupAfter 6:ThreadAllBefore 7:ThreadAllAfter! current value is: "
							+ methodTypeValue;
					log.error(errorMessage);
					throw new ConfigurationException(errorMessage);
				}
				methodTypeValues[i] = methodTypeValue;
			}
			return methodTypeValues;
		} catch (NumberFormatException e) {
			log.error("type attribute of group element should only be 0: NoneThreadGroup, 1: ThreadGroupFor! current value is: "
					+ methodType.trim());
			throw new ConfigurationException(e);
		}
	}

	/**
	 * build <code>Document</code> object based on specified config file
	 * a config file must have a root element, that is "suite", or "case",
	 * if doesn't have a root element, <code>JDOMParseException</code> will be thrown
	 * @param configFile
	 * @return Document
	 */
	private Document buildDocument(File configFile) {
		SAXBuilder builder = new SAXBuilder(false);
		try {
			return builder.build(configFile);
		} catch (Exception e) {
			log.error(
					"Error occurs when SAXBuilder build the XML config file: "
							+ configFile.getAbsolutePath(), e);
			throw new RuntimeException(e);
		}
	}

	/**
	 * External config file has a root element "case"
	 * @param configFile External config file
	 * @return Element return the "case" element of a configure file
	 */
	private Element getCaseElement(File configFile) {
		Document doc = buildDocument(configFile);
		// get all "case" elements of this config file
		return doc.getRootElement();
	}
	
	/**
	 * Extract all method objects defined under different annotation elements.<p>
	 * <li>like "allbefore", it may contains several methods: method1, method2, method3;
	 * <li>"allafter",  it may also contains several methods: method1, method2, method3;<p>
	 * so here we need to merge these three method objects, method1 will has "allbefore" and "allafter" annotation,
	 * so as method2 and method3.<p>
	 * finally, add these three methods to ThreadMethodContainer
	 * @param container ThreadMethodContainer
	 * @param targetClass The tast case class for which method object is extracted from 
	 * @param commonElement The common element
	 */
	private void handleCommonElement(ThreadMethodContainer container, Class<?> targetClass,
			Element commonElement) {
		// contains all the methods defined under "common" element
		List<ThreadMethod> threadMethods = new ArrayList<ThreadMethod>();
		for (Iterator<Class<? extends Annotation>> iterator = Utilities.COMMONANNOTATIONS
				.iterator(); iterator.hasNext();) {
			handleAnnotationElement(targetClass, commonElement, threadMethods, iterator.next());

		}
		add2Container(container, threadMethods);
	}

	/**
	 * Add each method of list to ThreadMethodContainer
	 * @param container
	 * @param threadMethods
	 */
	@SuppressWarnings("rawtypes")
	private void add2Container(ThreadMethodContainer container, List<ThreadMethod> threadMethods) {
		for (Iterator iterator = threadMethods.iterator(); iterator.hasNext();) {
			ThreadMethod threadMethod = (ThreadMethod) iterator.next();
			container.addThreadMethod(threadMethod);
		}
	}

	/**
	 * Extract all methods defined under annotation element related to specified annotationClass
	 * eg. methods under <allbefore> element
	 * @param targetClass
	 * @param commonElement
	 * @param threadMethods
	 */
	@SuppressWarnings("rawtypes")
	private void handleAnnotationElement(Class<?> targetClass, Element commonElement,
			List<ThreadMethod> threadMethods, Class annotationClass) {
		Element element = commonElement.getChild(Utilities
				.getAnnotationElementName(annotationClass));
		if (element != null) {
			Object annotation = AnnotationFactory.getAnnotationInstance(Utilities
					.getAnnotationType(annotationClass));
			Element methodsElement = element.getChild(MEHTODS_ELEMENT);
			List<ThreadMethod> methods = handleCommonMethodsElement(targetClass, methodsElement);
			setAnnotation(methods, annotation);
			// merge duplicate ThreadMethod's annotation from
			// threadAllBeforeMethods to threadMethods
			merge(threadMethods, methods);
		}
	}


	/**
	 * set this annotation instance into all the ThreadMethod objects contained in this List
	 * @param threadMethods
	 * @param annotation
	 */
	@SuppressWarnings("rawtypes")
	private void setAnnotation(List<ThreadMethod> threadMethods, Object annotation) {
		for (Iterator iterator = threadMethods.iterator(); iterator.hasNext();) {
			ThreadMethod threadMethod = (ThreadMethod) iterator.next();
			if (annotation instanceof ThreadGroupFor) {
				threadMethod.setThreadGroupFor((ThreadGroupFor) annotation);
			} else if (annotation instanceof ThreadBefore) {
				threadMethod.setThreadBefor((ThreadBefore) annotation);
			} else if (annotation instanceof ThreadAfter) {
				threadMethod.setThreadAfter((ThreadAfter) annotation);
			} else if (annotation instanceof ThreadGroupBefore) {
				threadMethod.setThreadGroupBefore((ThreadGroupBefore) annotation);
			} else if (annotation instanceof ThreadGroupAfter) {
				threadMethod.setThreadGroupAfter((ThreadGroupAfter) annotation);
			} else if (annotation instanceof ThreadAllBefore) {
				threadMethod.setThreadAllBefor((ThreadAllBefore) annotation);
			} else if (annotation instanceof ThreadAllAfter) {
				threadMethod.setThreadAllAfter((ThreadAllAfter) annotation);
			} else if (annotation instanceof ThreadFor) {
				threadMethod.setThreadFor((ThreadFor) annotation);
			}
		}

	}

	/**
	 * Handle methods under "common" element.<p>
	 * Extract each "method" element to a <code>Method</code> object, 
	 * then wrapper this method object into a <code>ThreadMethod</code> object
	 * @param targetClass
	 * @param methodsElement
	 * @return List<ThreadMethod>
	 */
	@SuppressWarnings({ "unchecked", "rawtypes" })
	private List<ThreadMethod> handleCommonMethodsElement(Class<?> targetClass,
			Element methodsElement) {
		// get all method element defined in this methods element
		List<Element> methodElements = methodsElement.getChildren(METHOD_ELEMENT);
		List<ThreadMethod> threadMethods = new ArrayList<ThreadMethod>();
		for (Iterator it = methodElements.iterator(); it.hasNext();) {
			Element methodElement = (Element) it.next();
			// get method name
			String methodName = methodElement.getAttributeValue(NAME_PROPERTY);

			// get parameter types and values
			List<Class> parameterTypes = new ArrayList<Class>();
			List<Object> parameterValues = new ArrayList<Object>();
			setMethodParameter(methodElement, parameterTypes, parameterValues);

			// get method object
			Method method = getMethod(targetClass, methodName, parameterTypes);

			if (parseFilter != null && !parseFilter.accept(method))
				continue;

			ThreadMethod threadMethod = new ThreadMethod(method);
			Object[] paramValues = new Object[parameterValues.size()];
			// set method parameters
			parameterValues.toArray(paramValues);
			threadMethod.setMethodParams(paramValues);

			threadMethods.add(threadMethod);
		}
		return threadMethods;
	}

	
	/**
	 * If any element of tms2Merge is contained in tms2MergedInto, fetch that element, 
	 * set it's not null annotation property into duplicate element in tms2MergedInto.
	 * Else, just add this element to tms2MergedInto
	 * @param tms2MergedInto
	 * @param tms2Merge
	 */
	@SuppressWarnings("rawtypes")
	private void merge(List<ThreadMethod> tms2MergedInto, List<ThreadMethod> tms2Merge) {
		for (Iterator iterator = tms2Merge.iterator(); iterator.hasNext();) {
			ThreadMethod threadMethod = (ThreadMethod) iterator.next();
			// To check if tms2MergedInto already contains this threadMethod
			int i = tms2MergedInto.indexOf(threadMethod);
			if (i != -1) {// do contained
				ThreadMethod tm2BeMergedInto = tms2MergedInto.get(i);
				// merge threadMethod's not null annotation property into
				// tm2BeMergedInto
				copyNotNullAnnotation(tm2BeMergedInto, threadMethod);
			} else {// not contains this ThreadMethod yet, just add it to
					// tms2MergedInto
				tms2MergedInto.add(threadMethod);
			}
		}

	}

	/**
	 * merge threadMethod's not null annotation property into tm2BeMergedInto
	 * @param tm2BeMergedInto
	 * @param threadMethod
	 */
	private void copyNotNullAnnotation(ThreadMethod tm2BeMergedInto, ThreadMethod threadMethod) {
		if (threadMethod.getThreadAfter() != null) {
			tm2BeMergedInto.setThreadAfter(threadMethod.getThreadAfter());
		}
		if (threadMethod.getThreadAllAfter() != null) {
			tm2BeMergedInto.setThreadAllAfter(threadMethod.getThreadAllAfter());
		}
		if (threadMethod.getThreadAllBefore() != null) {
			tm2BeMergedInto.setThreadAllBefor(threadMethod.getThreadAllBefore());
		}
		if (threadMethod.getThreadBefor() != null) {
			tm2BeMergedInto.setThreadBefor(threadMethod.getThreadBefor());
		}
		if (threadMethod.getThreadFor() != null) {
			tm2BeMergedInto.setThreadFor(threadMethod.getThreadFor());
		}
		if (threadMethod.getThreadGroupAfter() != null) {
			tm2BeMergedInto.setThreadGroupAfter(threadMethod.getThreadGroupAfter());
		}
		if (threadMethod.getThreadGroupBefore() != null) {
			tm2BeMergedInto.setThreadGroupBefore(threadMethod.getThreadGroupBefore());
		}
		if (threadMethod.getThreadGroupFor() != null) {
			tm2BeMergedInto.setThreadGroupFor(threadMethod.getThreadGroupFor());
		}
	}

	/**
	 * Log error message and throw a ConfigurationException
	 * @param errorMessage
	 */
	private void throwConfigurationException(String errorMessage) {
		log.error(errorMessage);
		throw new ConfigurationException(errorMessage);
	}

	/**
	 * Log error message and throw a ConfigurationException
	 * @param errorMessage
	 */
	private void throwConfigurationException(String errorMessage, Throwable e) {
		log.error(errorMessage, e);
		throw new ConfigurationException(errorMessage, e);
	}
	
	/**
	 * get external case file, this file should be a normal file,
	 * that means file is not a directory and else.
	 * @param suitePath String the path of top suite
	 * @param caseRefPath String the value of ref property
	 */
	private File getExternalFile(File suiteFile, String caseRefPath) {
		File file = new File(caseRefPath);
		if (file.exists()) {// try first direct path
			checkIsNormalFile(file);
			return file;
		}

		if(suiteFile==null || !suiteFile.exists()){
			String message = "Parent file: "+suiteFile+" not exists!";
			log.error(message);
			throw new ConfigurationException(message);
		}
		
		File parent = suiteFile.getParentFile();
		file = new File(parent, caseRefPath);
		if (file.exists()) {// try second relative path
			checkIsNormalFile(file);
			return file;
		}
		// file not found
		String message = "Could not find file with file name: " + caseRefPath;
		log.error(message);
		throw new ConfigurationException(new FileNotFoundException(message));
	}
	
	/**
	 * Check if this file is a normal file and not a directory and so on
	 * @param file
	 */
	private void checkIsNormalFile(File file){
		if(!file.isFile()){
			String message = "Specified file with filename: "+file.getAbsolutePath()+" should be a normal file, and cann't be a directory or else!";
			log.error(message);
			throw new ConfigurationException(message);
		}
	}


	/**
	 * entry config file must have a root element "suite"
	 * @param rootElement
	 */
	private void checkRootElement(Element rootElement) {
		if (!ROOT_ELEMENT.equalsIgnoreCase(rootElement.getName())) {
			String errorMessage = "A root element \"<suite>\" in this config file is mandatory! ";
			log.error(errorMessage);
			throw new ConfigurationException(errorMessage);
		}
	}

	/**
	 * First consider fileName is a full file path style, like "E:/projects/testframework/config.xml", 
	 * and try to construct a <code>File</code> object based on this fileName,
	 * if failed, consider this fileName is a relative file path, like "xxx.xml", 
	 * then try to construct this <code>File</code> object using relativeFile
	 * @param fileName
	 * @param fileName
	 * @return File the file object specified by the param fileName
	 */
	private File createFile(String fileName) {
		File configFile = null;
		try {
			configFile = new File(fileName);
			if (!configFile.exists())
				throw new FileNotFoundException("Can not find file: " + fileName);
		} catch (Exception e) {
			log.error("Error when create file with file name: " + fileName, e);
			throw new RuntimeException(e);
		}
		checkIsNormalFile(configFile);
		return configFile;
	}
	
	/**
	 * @param message
	 */
	private void debug(String message) {
		if (log.isDebugEnabled())
			log.debug(message);
	}
	
	/**
	 * @param message
	 */
/*	private void info(String message) {
		if (log.isInfoEnabled())
			log.info(message);
	}*/


	/**
	 * Find the <tt>Class</tt> object based on class name
	 * if not found, a <tt>ClassNotFoundException</tt> will be thrown
	 * @param className
	 * @return Class
	 */
	private Class<?> findClass(String className) {
		Class<?> clazz = null;
		try {
			clazz = Class.forName(className);
			return clazz;
		} catch (ClassNotFoundException e) {
			log.error("Error when try to find class: " + className, e);
			throw new RuntimeException(e);
		}
	}
	
}
